package com.sk.ccflower.server.lib.sockUtils.nio;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;

import org.apache.log4j.Logger;

public class SocketServer extends NioHandler
{
    private int m_listenPort;
    private String m_serviceHost = null;
    private NioSelector m_selector = null;
    private Class<?> m_clsSession = null;
    private ServerSocketChannel m_serverSocketChannel = null;

    protected final Logger m_log = Logger.getLogger(getClass());

    /**
     * localhost의 ip address 정보
     */
    private static InetAddress[] m_hostInetAddress = null;

    /**
     * hostname 정보
     */
    private static String m_hostname = null;

    /**
     * static member 초기화
     */
    static 	{
        InetAddress localInetAddress = null;
        InetAddress[] hostInetAddress = null;

        try {
            InetAddress localhost = InetAddress.getLocalHost();
            m_hostname = localhost.getHostName();
            localInetAddress = InetAddress.getByName("127.0.0.1");
            hostInetAddress = InetAddress.getAllByName(m_hostname);
        } catch (UnknownHostException e) {
        } finally {
            int count = 0;

            if(localInetAddress!=null) count ++;
            if(hostInetAddress!=null) count += hostInetAddress.length;

            if(count>0) {
                int i = 0;
                m_hostInetAddress = new InetAddress[count];
                if(localInetAddress!=null) m_hostInetAddress[i++] = localInetAddress;
                if(hostInetAddress!=null) {
                    for(int j=0; i<count && j<hostInetAddress.length; i++, j++) {
                        m_hostInetAddress[i] = hostInetAddress[j];
                    }
                }
            }
        }
    }

    /**
     * 생성자
     * @param listenPort: 서비스 listen 포트
     * @param clsSession: 클라이언트 접속시 생성되는 session 클래서, 반드시 SocketSession에서 상속받은 클래스이어야 한다.
     * @throws Exception: 입력되는 cls가 SocketSession에서 상속받은 클래스가 아닐경우 발생
     */
    public SocketServer(int listenPort, Class<?> clsSession) throws Exception
    {
        this(listenPort, clsSession, new NioSelector());
    }

    /**
     * 생성자
     * @param listenPort: 서비스 listen 포트
     * @param clsSession: 클라이언트 접속시 생성되는 session 클래서, 반드시 SocketSession에서 상속받은 클래스이어야 한다.
     * @param selector: 외부에서 입력받은 selector를 사용할 경우 생성한 selector instance
     * @throws Exception: 입력되는 cls가 SocketSession에서 상속받은 클래스가 아닐경우 발생
     */
    public SocketServer(int listenPort, Class<?> clsSession, NioSelector selector) throws Exception
    {
        if(!SocketSession.class.isAssignableFrom(clsSession)) throw new Exception("SocketSession에서 상속받은 클래스가 아닙니다.");

        if(selector!=null) m_selector = selector;
        else m_selector = new NioSelector();

        m_listenPort = listenPort;
        m_clsSession = clsSession;
    }

    /**
     * 생성자
     * @param host: 서비스를 제공하는 host
     * @param listenPort: 서비스 listen 포트
     * @param clsSession: 클라이언트 접속시 생성되는 session 클래서, 반드시 SocketSession에서 상속받은 클래스이어야 한다.
     * @throws Exception: 입력되는 cls가 SocketSession에서 상속받은 클래스가 아닐경우 발생
     */
    public SocketServer(String host, int listenPort, Class<?> clsSession) throws Exception
    {
        this(listenPort, clsSession);
        m_serviceHost = host;
    }

    /**
     * 생성자
     * @param host: 서비스를 제공하는 host
     * @param listenPort: 서비스 listen 포트
     * @param clsSession: 클라이언트 접속시 생성되는 session 클래서, 반드시 SocketSession에서 상속받은 클래스이어야 한다.
     * @param selector: 외부에서 입력받은 selector를 사용할 경우 생성한 selector instance
     * @throws Exception: 입력되는 cls가 SocketSession에서 상속받은 클래스가 아닐경우 발생
     */
    public SocketServer(String host, int listenPort, Class<?> clsSession, NioSelector selector) throws Exception
    {
        this(listenPort, clsSession, selector);
        m_serviceHost = host;
    }

    public static final String getHostname()
    {
        return m_hostname;
    }

    public static final InetAddress[] getHostInetAddress()
    {
        return m_hostInetAddress;
    }

    public static boolean isLocalAddress(String address)
    {
        if(address==null) return false;

        String addr = address.trim();

        if(m_hostInetAddress==null) {
            if(addr.equals("127.0.0.1") || addr.equals("localhost")) return true;
            else return false;
        } else {
            for(int i=0; i<m_hostInetAddress.length; i++) {
                if(m_hostInetAddress[i].getHostAddress().equals(addr)) return true;
            }
            return false;
        }
    }

    public NioSelector getSelector()
    {
        return m_selector;
    }

    /**
     * 서비스를 시작한다.
     */
    public void run() throws Exception
    {
        try {
            init();
            m_selector.run();
        } catch(Exception e) {
            throw new Exception(e);
        } finally {
            close();
        }
    }

    public void broadcast(byte[] b)
    {
        boroadcast(b, 0, b.length);
    }

    /**
     * 모든 session에 메시지를 전송한다.
     */
    public void boroadcast(byte[] b, int offset, int length)
    {
        Object obj = null;
        SelectionKey key = null;
        SelectableChannel channel = null;
        SocketSession session = null;
        Iterator<SelectionKey> itr = m_selector.getKeys().iterator();

        while(itr.hasNext())
        {
            key = itr.next();
            channel = key.channel();

            if(!key.isValid()) continue;

            obj = key.attachment();

            if(channel instanceof SocketChannel && obj instanceof SocketSession)
            {
                try {
                    if(session!=null) ((SocketSession)obj).send(b, offset, length);
                } catch (Exception e) {
                }
            }
        }
    }

    /**
     * 모든 session을 닫고 listen socket을 닫는다.
     */
    public void close()
    {
        SelectionKey key = null;
        SelectableChannel channel = null;
        SocketSession session = null;
        Iterator<SelectionKey> itr = m_selector.getKeys().iterator();
        try{
            while( itr.hasNext() ){
                key=itr.next();
                channel=key.channel();

                try{
                    if( channel instanceof SocketChannel&&session instanceof SocketSession ){
                        session=( SocketSession )key.attachment();
                        if( session!=null )
                            session.close();
                        else
                            channel.close();
                    }
                } catch( IOException e ){
                }
            }

            try{
                m_serverSocketChannel.close();
            } catch( IOException e ){
            }
        }catch(Exception e){}
    }

    @Override
    protected void handleInput(SelectionKey key)
    {
        if(!key.isAcceptable()) {
            m_log.fatal("key is not acceptable !!");
            m_selector.endSelect();
            return;
        }

        try {
            accept(key);
        } catch (Exception e) {
            m_log.fatal("accept error", e);
            m_selector.endSelect();
            return;
        }
    }

    @Override
    protected void handleTimeout(SelectionKey key, Date date)
    {
        doTimeoutAction(date);
    }

    /**
     * 등록한 Timeout에 의해 timeout이 발생하면 호출됨, 하위 클래스에서 재 정의하여 사용한다.
     */
    protected void doTimeoutAction(Date date)
    {
    }

    /**
     * listen socket을 생성한다.
     */
    private void init() throws Exception
    {
        m_log.info("service port = " + m_listenPort);

        InetSocketAddress isa = null;

        if(m_serviceHost==null) isa = new InetSocketAddress(m_listenPort);
        else isa = new InetSocketAddress(InetAddress.getByName(m_serviceHost), m_listenPort);

        m_serverSocketChannel = ServerSocketChannel.open();

        m_serverSocketChannel.socket().setReuseAddress(true);
        m_serverSocketChannel.socket().bind(isa);
        m_serverSocketChannel.configureBlocking(false);

        m_selector.registerHandler(this, m_serverSocketChannel, SelectionKey.OP_ACCEPT);

        m_log.info("service started : port = " + m_serverSocketChannel.socket().getLocalPort());
    }

    /**
     * 새로운 session을 생성한다.
     */
    private void accept(SelectionKey key) throws Exception
    {
        ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
        SocketChannel sc = ssc.accept();

        try {
            newSocketSession(sc);
        } catch (SecurityException e) {
            m_log.error("세션 생성 실패", e);
        } catch (IllegalArgumentException e) {
            m_log.error("세션 생성 실패", e);
        } catch (NoSuchMethodException e) {
            m_log.error("세션 생성 실패", e);
        } catch (InstantiationException e) {
            m_log.error("세션 생성 실패", e);
        } catch (IllegalAccessException e) {
            m_log.error("세션 생성 실패", e);
        } catch (InvocationTargetException e) {
            m_log.error("세션 생성 실패", e);
        } catch (Exception e) {
            m_log.error("세션 생성 실패", e);
        }
    }

    /**
     * 입력된 Session의 클래스 타입으로 새로운 session을 생성한다.
     */
    private SocketSession newSocketSession(SocketChannel sc) throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, Exception
    {
        Constructor<?> cstSocketSession = m_clsSession.getConstructor(SocketChannel.class, SocketServer.class);
        return (SocketSession)cstSocketSession.newInstance(sc, this);
    }


    /*
    public static void main(String[] args)
    {
        try {
            SocketServer sv = new SocketServer(9000, SocketSession.class);
            sv.run();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    */
}
